"use client"; import { useState } from "react"; import { useParams, useRouter } from "next/navigation"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { apiFetch, pageApi } from "@/lib/api"; import { useAuth } from "@/hooks/use-auth"; import { useWorkspace } from "@/hooks/use-workspace"; import { toast } from "@/stores/ui-store"; import { Field, Input } from "@/components/ui/form"; import { PERM_LEVELS, type ShareLevel } from "@/lib/constants"; import type { WorkspaceRole } from "@/lib/types"; const LEVEL_OPTIONS: { value: ShareLevel; label: string; description: string }[] = [ { value: "view", label: "Can view", description: "Read-only access to this page." }, { value: "edit", label: "Can edit", description: "Can edit this page and its sub-pages." }, { value: "public", label: "Anyone with the link", description: "No sign-in required." } ]; /** * /w/:workspace/page/:id/share — page sharing surface (route form of the * share dialog): public link + permission level + invite by email. */ export default function PageShareRoute() { const params = useParams<{ id: string; workspace: string }>(); const pageId = params?.id ?? ""; const slugOrId = params?.workspace ?? ""; const router = useRouter(); const qc = useQueryClient(); const { user } = useAuth(); const wsQuery = useWorkspace(slugOrId); // TASK-0133 — the org policy's guest-public-share gate (the member-facing // effective endpoint, never authoritative). Shown as a notice when the org // blocks "Anyone with the link"; the API is the enforcement (403). const effectivePolicy = useQuery({ queryKey: ["workspaces", wsQuery.data?.id, "policy", "effective"], queryFn: async () => { const res = await apiFetch<{ policy: { disableGuestPublicShare?: boolean } }>( `/workspaces/${wsQuery.data?.id}/policy/effective` ); return res.policy; }, enabled: Boolean(wsQuery.data?.id), retry: false }); const guestPublicBlocked = effectivePolicy.data?.disableGuestPublicShare === true; // The page share offers the three LINK levels the endpoint accepts // (member roles comment/full_edit are not link levels — see ShareLevel). const [level, setLevel] = useState<"view" | "edit" | "public">("view"); const [email, setEmail] = useState(""); const [role, setRole] = useState("member"); const [copied, setCopied] = useState(false); const share = useQuery({ queryKey: ["pages", pageId, "share"], queryFn: async () => { try { return await pageApi.share(pageId, { level: "view" }); } catch { // No share link yet → the mutation below creates one. return null; } }, enabled: Boolean(pageId), retry: false }); const saveShare = useMutation({ mutationFn: (input: { level: "view" | "edit" | "public" }) => pageApi.share(pageId, input), onSuccess: (data) => { qc.setQueryData(["pages", pageId, "share"], data); } }); const invite = useMutation({ mutationFn: (input: { email: string; role: WorkspaceRole }) => apiFetch<{ ok: true }>(`/pages/${pageId}/invites`, { method: "POST", body: input }) }); const shareUrl = share.data?.url ?? (pageId ? `${typeof window !== "undefined" ? window.location.origin : ""}/f/${pageId}` : ""); const copyLink = async () => { try { await navigator.clipboard.writeText(shareUrl); setCopied(true); setTimeout(() => setCopied(false), 2000); } catch { toast("error", "Copy failed", "Select the link text and copy manually."); } }; return (

Share

Control who can access this page.

Public link

{guestPublicBlocked && (

Your organization has disabled public (guest) sharing. “Anyone with the link” is not available — the server blocks it.

)}
{LEVEL_OPTIONS.map((opt) => { // `LEVEL_OPTIONS` is typed `ShareLevel` (member ladder + the // "public" guest link the endpoint also accepts); // compare through `string` to stay type-safe without widening the // page's (out-of-scope) type system. const value = opt.value as string; const blocked = guestPublicBlocked && value === "public"; return ( ); })}

Invite people

setEmail(e.target.value)} />

{PERM_LEVELS.length} permission levels are enforced server-side — the frontend only renders what your role allows (never the source of truth).

); }